﻿using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Nuclex.Game.Packing;
using Xnatlas.Serialization;

namespace Xnatlas
{
    public abstract class BaseAtlas
    {
        private readonly List<AtlasedTexture> _comingTextures = new List<AtlasedTexture>();
        private float _widthDiv;
        private float _heightDiv;
        private float _margin;

        protected BaseAtlas()
        {
            Textures = new List<AtlasedTexture>();
            Dictionary = new Dictionary<string, AtlasedTexture>();
        }

        protected BaseAtlas(int width, int height)
            : this()
        {
            SetSize(width, height);
        }

        public bool Locked { get; private set; }

        public int Width { get; set; }
        public int Height { get; set; }
        public List<AtlasedTexture> Textures { get; set; }
        protected Dictionary<string, AtlasedTexture> Dictionary { get; set; }

        public AtlasedTexture GetOrEnqueueTexture(string name, string data = null)
        {
            if (!Locked)
            {
                return EnqueueTexture(name, data);
            }
            else
            {
                return GetAtlasedTexture(name);
            }
        }

        public AtlasedTexture GetAtlasedTexture(string name)
        {
            return Dictionary[name];
        }

        public void Lock()
        {
            Locked = true;
        }

        public AtlasedTexture EnqueueTexture(string file, string data = null)
        {
            return EnqueueTexture(file, GetTextureWrapper(file, data));
        }

        public AtlasedTexture EnqueueTexture(string file, ITextureWrapper textureWrapper)
        {
            if (Locked)
            {
                throw new InvalidOperationException("Unable to load texture to locked atlas!");
            }

            AtlasedTexture atlasedTexture =
                new AtlasedTexture(file, textureWrapper);

            _comingTextures.Add(atlasedTexture);
            return atlasedTexture;
        }

        public void AppendQueuedTextures(bool addMargin = true)
        {
            if (Locked)
            {
                return;
            }

            Locked = true;

            RectanglePacker rectanglePacker = new CygonRectanglePacker(Width, Height);
            List<AtlasedTexture> sortedTextures = _comingTextures.OrderByDescending(x => x.Width * x.Height).ToList();
            foreach (AtlasedTexture atlasedTexture in sortedTextures)
            {
                int margin = addMargin ? 1 : 0;
                Point point = rectanglePacker.Pack(atlasedTexture.Width + margin * 2, atlasedTexture.Height + margin * 2);
                AddAtlasedTexture(atlasedTexture, new Point(point.X + margin, point.Y + margin));
            }
        }

        protected AtlasStore CreateAtlasStore()
        {
            AtlasStore atlasStore = new AtlasStore { Width = Width, Height = Height };
            foreach (AtlasedTexture atlasedTexture in Dictionary.Values)
            {
                AtlasStoreBitmap atlasStoreBitmap =
                    new AtlasStoreBitmap
                    {
                        FileName = atlasedTexture.FileName,
                        AtlasAreaRectangle = atlasedTexture.AtlasAreaRectangle,
                        Data = atlasedTexture.OriginalTexture.Data
                    };

                atlasStore.Bitmaps.Add(atlasStoreBitmap);
            }
            return atlasStore;
        }

        protected abstract ITextureWrapper GetTextureWrapper(string name, string data);
        protected abstract void BlitTextureToAtlas(AtlasedTexture atlasedTexture);

        protected void UpdateUvCoordinates(AtlasedTexture atlasedTexture)
        {
            atlasedTexture.UvCoordinateTopLeft =
                new Vector2(
                    atlasedTexture.AtlasAreaRectangle.X * _widthDiv + _margin,
                    atlasedTexture.AtlasAreaRectangle.Y * _heightDiv + _margin);

            atlasedTexture.UvCoordinateBottomRight =
                new Vector2(
                    (atlasedTexture.AtlasAreaRectangle.X + atlasedTexture.AtlasAreaRectangle.Width) * _widthDiv - 2 * _margin,
                    (atlasedTexture.AtlasAreaRectangle.Y + atlasedTexture.AtlasAreaRectangle.Height) * _heightDiv - 2 * _margin);
        }

        protected void AppendStoreBitmaps(AtlasStore atlasStore)
        {
            foreach (AtlasStoreBitmap atlasStoreBitmap in atlasStore.Bitmaps)
            {
                AtlasedTexture atlasedTexture =
                    new AtlasedTexture(atlasStoreBitmap.FileName, atlasStoreBitmap)
                    {
                        AtlasAreaRectangle = atlasStoreBitmap.AtlasAreaRectangle
                    };
                AppendAtlastedTexture(atlasedTexture);
            }
        }

        protected void SetSize(int width, int height)
        {
            Width = width;
            Height = height;

            _margin = 0.5f / Width;
            _widthDiv = 1f / Width;
            _heightDiv = 1f / Height;
        }

        private void AppendAtlastedTexture(AtlasedTexture atlasedTexture)
        {
            UpdateUvCoordinates(atlasedTexture);
            Dictionary.Add(atlasedTexture.FileName, atlasedTexture);
            Textures.Add(atlasedTexture);
        }

        private void AddAtlasedTexture(AtlasedTexture atlasedTexture, Point point)
        {
            AddAtlasedTexture(atlasedTexture, new Rectangle(point.X, point.Y, atlasedTexture.Width, atlasedTexture.Height));
        }

        private void AddAtlasedTexture(AtlasedTexture atlasedTexture, Rectangle rect)
        {
            atlasedTexture.AtlasAreaRectangle = rect;
            BlitTextureToAtlas(atlasedTexture);

            AppendAtlastedTexture(atlasedTexture);
        }
    }
}